Skip to content

add pinocchio spl-token-minter example#600

Open
MarkFeder wants to merge 3 commits into
solana-foundation:mainfrom
MarkFeder:tokens-spl-token-minter-pinocchio
Open

add pinocchio spl-token-minter example#600
MarkFeder wants to merge 3 commits into
solana-foundation:mainfrom
MarkFeder:tokens-spl-token-minter-pinocchio

Conversation

@MarkFeder

Copy link
Copy Markdown
Contributor

Adds a Pinocchio implementation of tokens/spl-token-minter, continuing the Pinocchio token-examples lane after transfer-tokens (#596), escrow (#598), and create-token (#599).

What it does

Two instructions, matching the anchor and native options:

  • Create — creates an SPL Token mint (9 decimals) and attaches an on-chain Metaplex metadata account (name, symbol, URI).
  • Mint — mints tokens into the payer's associated token account, creating that account first if needed.

Notes

  • Instructions are dispatched by a leading discriminator byte (0 = Create, 1 = Mint), matching the Borsh enum variant index used by the native example.
  • The Metaplex Token Metadata program has no typed Pinocchio crate, so the CreateMetadataAccountV3 CPI is built by hand (raw InstructionView / cpi::invoke) — the same approach as add pinocchio create-token example #599.
  • The bankrun test loads the Metaplex program from a mainnet-dumped token_metadata.so fixture via a postinstall script, like the anchor example.
  • The mint authority is passed as a non-signer and aliased to the payer (which signs), mirroring the native example.

Files

  • New: tokens/spl-token-minter/pinocchio/ (program + bankrun test)
  • Edited: root Cargo.toml (workspace member), README.md (pinocchio link), Cargo.lock

@MarkFeder
MarkFeder force-pushed the tokens-spl-token-minter-pinocchio branch from 2819844 to 383287d Compare July 8, 2026 21:13
@greptile-apps

greptile-apps Bot commented Jul 8, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds a Pinocchio implementation of the spl-token-minter example, continuing the Pinocchio token lane. It provides two instructions — Create (initialises an SPL Token mint and attaches a hand-crafted Metaplex CreateMetadataAccountV3 CPI) and Mint (idempotent ATA creation + MintTo) — backed by bankrun tests that load the Metaplex program from a mainnet fixture.

  • create.rs manually serialises the Metaplex instruction data (discriminator 33, Borsh DataV2, is_mutable, collection_details) using push_borsh_string, bypassing any Metaplex typed crate — the same pattern used by the sibling create-token example.
  • mint.rs uses pinocchio-associated-token-account::CreateIdempotent then pinocchio-token::MintTo, with the payer's key aliased to the mint_authority slot (non-signer) to satisfy the SPL Token signer requirement via the payer's existing signature.
  • Lamports for the mint account are computed with integer arithmetic using ACCOUNT_STORAGE_OVERHEAD and DEFAULT_LAMPORTS_PER_BYTE (which already folds in the 2-year exemption factor), avoiding the floating-point path that bankrun rejects.

Confidence Score: 5/5

Safe to merge — all instruction logic, CPI account layouts, Borsh serialisation, and lamport arithmetic are correct; bankrun tests pass both the Create and Mint paths end-to-end.

The implementation is a clean, well-commented port of the native example. The raw Metaplex CPI serialisation (discriminator, DataV2, option flags) matches the wire format, the DEFAULT_LAMPORTS_PER_BYTE constant correctly includes the 2-year exemption factor, and the payer-as-mint-authority privilege pattern is intentional and documented. The only finding is a minor behavioural difference in how the update-authority signer flag is communicated to Metaplex, which has no effect on correctness here.

No files require special attention.

Important Files Changed

Filename Overview
tokens/spl-token-minter/pinocchio/program/src/instructions/create.rs Implements mint-account creation + Metaplex metadata CPI; lamport math, Borsh serialization layout, and account ordering all look correct. One minor semantic difference from the native example in how update_authority signer flag is communicated to Metaplex.
tokens/spl-token-minter/pinocchio/program/src/instructions/mint.rs Idempotent ATA creation followed by MintTo CPI; account ordering and privilege escalation via payer==mint_authority pattern are correct.
tokens/spl-token-minter/pinocchio/program/src/instructions/mod.rs Borsh string parser handles bounds checks correctly; parse_u64 is straightforward and safe.
tokens/spl-token-minter/pinocchio/program/src/processor.rs Clean discriminator-based dispatch matching the native example's Borsh enum variant indices.
tokens/spl-token-minter/pinocchio/tests/test.ts Bankrun tests cover both Create and Mint paths; u64le encoding, metadata address derivation, and ATA amount check at offset 64 are all correct. Previous BigInt concern addressed via inline comment.
tokens/spl-token-minter/pinocchio/prepare.mjs Dumps Metaplex program from mainnet using per-command -um flag (no global config mutation); errors are non-fatal as documented.
tokens/spl-token-minter/pinocchio/program/src/lib.rs Standard no_std entrypoint setup with bump allocator and panic handler.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client
    participant Program as spl-token-minter<br/>(pinocchio)
    participant System as System Program
    participant Token as SPL Token Program
    participant Metaplex as Token Metadata Program
    participant ATA as Associated Token Program

    Note over Client,ATA: Create Instruction (discriminator = 0)
    Client->>Program: create(name, symbol, uri)
    Program->>System: CreateAccount(mint, lamports, 82 bytes)
    System-->>Program: ok
    Program->>Token: "InitializeMint2(decimals=9, mint_authority=payer)"
    Token-->>Program: ok
    Program->>Metaplex: "CreateMetadataAccountV3(raw CPI, disc=33)"
    Metaplex->>System: allocate metadata PDA
    System-->>Metaplex: ok
    Metaplex-->>Program: ok
    Program-->>Client: success

    Note over Client,ATA: Mint Instruction (discriminator = 1)
    Client->>Program: mint(quantity)
    Program->>ATA: CreateIdempotent(payer, mint to ATA)
    ATA->>System: create ATA if needed
    System-->>ATA: ok
    ATA-->>Program: ok
    Program->>Token: "MintTo(mint, ATA, mint_authority=payer, amount)"
    Token-->>Program: ok
    Program-->>Client: success
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Client
    participant Program as spl-token-minter<br/>(pinocchio)
    participant System as System Program
    participant Token as SPL Token Program
    participant Metaplex as Token Metadata Program
    participant ATA as Associated Token Program

    Note over Client,ATA: Create Instruction (discriminator = 0)
    Client->>Program: create(name, symbol, uri)
    Program->>System: CreateAccount(mint, lamports, 82 bytes)
    System-->>Program: ok
    Program->>Token: "InitializeMint2(decimals=9, mint_authority=payer)"
    Token-->>Program: ok
    Program->>Metaplex: "CreateMetadataAccountV3(raw CPI, disc=33)"
    Metaplex->>System: allocate metadata PDA
    System-->>Metaplex: ok
    Metaplex-->>Program: ok
    Program-->>Client: success

    Note over Client,ATA: Mint Instruction (discriminator = 1)
    Client->>Program: mint(quantity)
    Program->>ATA: CreateIdempotent(payer, mint to ATA)
    ATA->>System: create ATA if needed
    System-->>ATA: ok
    ATA-->>Program: ok
    Program->>Token: "MintTo(mint, ATA, mint_authority=payer, amount)"
    Token-->>Program: ok
    Program-->>Client: success
Loading

Reviews (4): Last reviewed commit: "spl-token-minter pinocchio: make bankrun..." | Re-trigger Greptile

try {
mkdirSync(outputDir, { recursive: true });
// Point the Solana CLI at mainnet, where the canonical program lives.
execSync("solana config set -um", { stdio: "inherit" });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 postinstall mutates the global Solana CLI config

solana config set -um permanently sets the user's active cluster to mainnet-beta in ~/.config/solana/cli/config.yml. Any developer who runs pnpm install in this directory while working against devnet or localnet will silently have their Solana CLI endpoint changed. solana program dump accepts a --url flag (--url mainnet-beta or --url m), which would fetch from mainnet without touching the global config.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in dd291b9. Switched to a per-command solana program dump -um <id> and dropped the global solana config set -um, so postinstall no longer mutates the developer's ~/.config/solana/cli/config.yml. This mirrors the pattern already used in the sibling pda-mint-authority example.

Comment on lines +46 to +52
MintTo {
mint: mint_account,
account: associated_token_account,
mint_authority,
amount: quantity,
}
.invoke()?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Implicit mint_authority == payer invariant is not asserted

MintTo requires its mint_authority to be a signer. Here mint_authority (account index 1) is declared as a non-signer ([]) in the instruction — the CPI works only because the payer's address is passed for both slots and the payer signs the transaction. If a caller provides a genuinely different mint_authority key, the SPL Token program rejects the CPI with an opaque "missing required signature" error instead of a clear program-level message. The same applies to create.rs. For an educational example a quick equality check would make the design intent explicit and produce a cleaner error for learners who experiment with different account layouts.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentional for this example. It models the canonical "payer is the mint authority" flow: the same key fills both the payer and mint_authority slots, and since the payer already signs the transaction the MintTo/InitializeMint CPIs satisfy the signer requirement — which is why mint_authority is marked non-signer ([]) here. The SPL Token program is the authority on that invariant and enforces it, so an added equality check would be redundant and would diverge from the anchor example this port mirrors. Happy to add an explicit assert if you'd prefer it for teaching clarity.

Comment on lines +53 to +56
function readTokenAmount(data: Uint8Array): number {
const buffer = Buffer.from(data);
return buffer.readUInt32LE(64) + buffer.readUInt32LE(68) * 4294967296; // 2^32
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 readTokenAmount silently loses precision for amounts above 2^53

The high-word multiplication buffer.readUInt32LE(68) * 4294967296 uses JavaScript's number type, which only holds integers exactly up to 2^53. Token amounts are stored as raw u64 units: 150 tokens at 9 decimals = 1.5 × 10^11, well under 2^53 and safe here. But if a future test mints a much larger amount, the assertion could silently pass with a wrong value. The u64le helper already acknowledges this ceiling; readTokenAmount should carry a similar comment, or the check should use BigInt arithmetic.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in dd291b9 via your suggested alternative — added a comment mirroring the u64le encoder, documenting the deliberate es6/no-BigInt design and the exact-below-2^53 guarantee (the 150-token mint asserted here is well within range). Kept number for symmetry with the encoder rather than introducing BigInt on only the decode side.

@MarkFeder
MarkFeder force-pushed the tokens-spl-token-minter-pinocchio branch from dd291b9 to c6690d5 Compare July 9, 2026 07:38
@MarkFeder

Copy link
Copy Markdown
Contributor Author

@Perelyn-sama @dev-jodee — rebased onto latest main (picks up the ASM sbpf/Solana pin from #625), CI is now fully green. Ready for review whenever you have a chance 🙏

Move the async bankrun setup out of the `describe` callback and into a
`before` hook so Mocha collects the `it` blocks (an async `describe` body
registers tests after the suite is already collected, so nothing ran).

With the test now executing, replace `Rent::try_minimum_balance` with the
integer rent formula: its floating-point exemption-threshold path emits an
opcode the bankrun VM rejects ("unsupported BPF instruction"). Matches the
create-token example.
@MarkFeder
MarkFeder requested a review from dev-jodee as a code owner July 15, 2026 21:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant